/**********************************
 * Assignment 5: Alien vs Predator
 * Date: 12/04/2011
 *
 * Alexander Miller: ahm82, 2605797
 * David Felty: djf242, 2828357
 * David Hau: dch229, 2586217
 * Hanna Atmer: ha237, 2666426
 **********************************/

package avp.predator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;

import avp.Agent;
import avp.Edge;
import avp.EdgeState;
import avp.Info;
import avp.Move;
import avp.Node;
import avp.Ship;

public class Predator implements Agent {
	
	enum PredatorState {
		RUSHING, TRAILING, BLOCKING, FORTIFYING
	}
	
	enum FortifyState {
		CLOSING, MOVING, RETURNING
	}
	
	private Ship ship;
	private PredatorState state;
	private FortifyState fortstate;
	private Node currentLocation;
	//private boolean alienNearby = false;
	//private boolean alienNearby2 = false;
	private Edge blockedEdge;
	private int blockedCounter = 0;
	
	final int OPEN_TIME = 1;
	final int PRED_SENSE = 5;
	final int WAIT_TIME = 0;
	final int TRAIL_DISTANCE = 1000;
	final int TRAIL_PAUSE_RATE = 1000;
	static int AVOID_WEIGHT = 300;
	final int AVOID_ADJUST = 200;
	
	Set<Edge> avoidedEdges = new HashSet<Edge>();
	HashMap<Edge,Integer> weights = new HashMap<Edge,Integer>();
	
	public Predator() {
		state = PredatorState.RUSHING;
	}

	@Override
	public void init(Ship ship) {
		if (ship != null) {
			this.ship = ship;
		}
		updateWeights();
	}

	@Override
	public Move nextMove(Info info) {
		// update the ship map
		if (info.atScanner) init(info.ship); // uses the scanned ship if the agent has the scanner
		
		if (!info.atScanner) {
			for (Edge e : ship.getEdges()) { for (Edge a : info.adjacentEdges) { if (e.equals(a)) { e.setState(a.getState()); } } }
		}
		for (Node n : ship.getNodes()) {
			if (n.equals(info.location)) currentLocation = n;
		}
		//figure out what attitude the AI should have
		if (info.adversaryPresent) state = PredatorState.BLOCKING;
		else if ((info.atControlRoom || state == PredatorState.FORTIFYING) && fortstate != FortifyState.RETURNING) state = PredatorState.FORTIFYING;
		/*else if (alienNearby || alienNearby2 ||
				ship.dijkstra(ship.getPredatorLocation(), ship.getControlRoom()).size() < TRAIL_DISTANCE &&
				ship.dijkstra(ship.getPredatorLocation(), ship.getControlRoom()).size() % TRAIL_PAUSE_RATE != 0 &&
				fortstate != FortifyState.RETURNING) state = PredatorState.TRAILING;*/
		else state = PredatorState.TRAILING;
		
		//if (alienNearby) alienNearby = false;
		//else alienNearby2 = false;
		
		Move m = new Move();
		
		if (state == PredatorState.RUSHING) { 
			m.edge = getRealEdge(info, dijkstra((!info.atScanner) ? currentLocation : ship.getPredatorLocation(), ship.getControlRoom()).get(0));

			if (m.edge.getState() == EdgeState.CLOSED) {
				m.move = false;
				m.changeState = true;
			}
			else {
				m.move = true;
				m.changeState = false;
			}
			if (checkBlockedEdge(m)) {
				m.edge = null;
				m.move = false;
				m.changeState = false;
//				System.out.println("WAITING");
			}
			
			return m;
		}
		else if (state == PredatorState.TRAILING) {
			m.edge = getRealEdge(info, dijkstra((!info.atScanner) ? currentLocation : ship.getPredatorLocation(), ship.getControlRoom()).get(0));
			if (m.edge.getState() == EdgeState.CLOSED) {
				m.move = false;
				m.changeState = true;
			}
			else {
				m.move = true;
				m.changeState = (m.edge.getState() != EdgeState.BURNED);
			}
			if (checkBlockedEdge(m)) {
				m.edge = null;
				m.move = false;
				m.changeState = false;
				//System.out.println("WAITING");
			}
			return m;
		}
		else if (state == PredatorState.BLOCKING) {
			//alienNearby = true;
			//alienNearby2 = true;
			m.edge = info.adversaryDirection;
			avoidedEdges.add(getFakeEdge(m.edge));
			blockedEdge = m.edge;
			blockedCounter = WAIT_TIME;
			AVOID_WEIGHT += AVOID_ADJUST;
			if (m.edge.getState() != EdgeState.BURNED) {
				m.move = false;
				m.changeState = true;
			}
			else {
				if (!info.atControlRoom) m = getRunningMove(info);
				else {
					m.edge = info.adversaryDirection;
					m.move = true;
					m.changeState = false;
				}
			}
			return m;
		}
		else if (state == PredatorState.FORTIFYING) {
			if (getNextDoor(info) != null) fortstate = FortifyState.CLOSING;
			else if (getNextMove(info) != null) fortstate = FortifyState.MOVING;
			else fortstate = FortifyState.RETURNING;
			
			if (fortstate == FortifyState.CLOSING) {
				m.edge = getNextDoor(info);
				m.move = false;
				m.changeState = true;
				return m;
			}
			else if (fortstate == FortifyState.MOVING) {
				m.edge = getNextMove(info);
				m.move = true;
				m.changeState = false;
				return m;
			}
			else return nextMove(info);
		}
		
		else return null; // should never be reached, if null is returned then there is an error
	}

	/**
	 * Sets the weights of the edges of the ship based on their EdgeState
	 */
	private void updateWeights() {
		weights.clear();
		
		for (Edge e : ship.getEdges()) {
			if (e.getState() == EdgeState.CLOSED) weights.put(e, 1 + OPEN_TIME);
			else if (e.getState() == EdgeState.OPEN || e.getState() == EdgeState.BURNED) weights.put(e, 1);
			else if (e.getState() == null) weights.put(e, Integer.MAX_VALUE / 2);
		}
		
		for (Edge a : avoidedEdges) {
			weights.remove(a);
			weights.put(a, AVOID_WEIGHT);
		}
	}
	
	private Edge getNextDoor(Info info) {
		for (Edge e : info.adjacentEdges) {
			if (e.getState() == EdgeState.OPEN) return e;
		}
		return null;
	}
	
	private Edge getNextMove(Info info) {
		for (Edge e : info.adjacentEdges) {
			if (e.getState() == EdgeState.BURNED) return e;
		}
		return null;
	}
	
	/**
	 * @return move according to new path to control room now that info.adversaryPresent is weighted according to AVOID_WEIGHT
	 * Will open a closed door if necessary, otherwise will move through the space. Closes door behind it unless edge is burned.
	 */
	private Move getRunningMove(Info info) {
		Move m = new Move();
		
		updateWeights();
		
		m.edge = getRealEdge(info, dijkstra((!info.atScanner) ? currentLocation : ship.getPredatorLocation(), ship.getControlRoom()).get(0));
		if (m.edge.getState() == EdgeState.CLOSED) {
			m.move = false;
			m.changeState = true;
		}
		else {
			m.move = true;
			m.changeState = (m.edge.getState() != EdgeState.BURNED);
		}
		
		return m;
	}
	
	private Edge getRealEdge(Info info, Edge edge) {
		for (Edge e : info.allEdges) {
			if (e.equals(edge)) return e;
		}
		return null;
	}
	
	private Edge getFakeEdge(Edge edge) {
		for (Edge e : ship.getEdges()) {
			if (e.equals(edge)) return e;
		}
		return null;
	}
	
	private boolean checkBlockedEdge(Move move) {
		if (blockedCounter == 0) blockedEdge = null;
		else blockedCounter--;
		return (blockedEdge != null && move.edge.equals(blockedEdge));
	}
	
	/**
	 * Method is disabled using return statement due to poor results in testing.
	 */
/*	private void scanWeights() {
		if (true) return;
		Map<Node,Integer> dist = ship.distFromNode(ship.predatorNode);
		
		for (Node n : ship.getNodes()) {
			for (Edge e : n.getAdjacent()) {
				if (dist.get(n.getOtherThru(e)) <= GameConstants.PRED_SENSE/2 + 1) {
					e.weight += (GameConstants.PRED_SENSE/2 - (dist.get(n.getOtherThru(e)) + 1))/2;
				}
			}
		}
	}*/
	
	public PredatorState getState() {
		return state;
	}
	
	public String ident() {
		return "Uday";
	}
	
	private List<Edge> dijkstra(Node start, Node end) {
		updateWeights();
		return dijkstraBase(start, end, new HashMap<Node,Integer>());
	}
	
	// Finds the lowest cost path between start and end; returns null if no path exists
	private List<Edge> dijkstraBase(Node start, Node end, final Map<Node,Integer> dist) {
		start = getEqualNode(start);
		final Set<Node> marked = new HashSet<Node>();
		final Map<Node,Node> prev = new HashMap<Node,Node>();
		final Queue<Node> q = new PriorityQueue<Node>(16, new Comparator<Node>() {
			public int compare(Node n1, Node n2) {
				return dist.get(n1) - dist.get(n2);
			}
		});
		dist.put(start, 0);
		q.add(start);
		
		while (!q.isEmpty()) {
			Node u = q.poll();
			if (u.equals(end)) break;
			marked.add(u);
			Integer du = dist.get(u); 					// du = distance to u
			if (du == null) break; 						// We reached a node that is infinity
			
			for (Node v : getAdjacentNodes(u)) {
				if (marked.contains(v)) continue;
				Integer dv = dist.get(v); 				// dv = distance to v
				int alt = du + weights.get(edgeTo(u, v));		// alt = alternate dist to v
				if (dv == null || alt < dv) {			// null dv represents infinity
					dist.put(v, alt);					// Update new shortest path to v
					prev.put(v, u);
					q.remove(v);						// Reorder v in q
					q.add(v);
				}
			}
		}
		
		return backtrack(start, end, prev);
	}
	
	// Backtracks through prev from end to start, returning the path of edges in between
	private List<Edge> backtrack(Node start, Node end, Map<Node,Node> prev) {
		if (!prev.containsKey(end)) return null;
		
		List<Edge> es = new ArrayList<Edge>();
		Node a = end;
		Node b = prev.get(a);
		while (b != null) {
			es.add(edgeTo(a, b));
			a = b;
			b = prev.get(a);
		}
		Collections.reverse(es); // Reverse the backtrack
		return es;
	}
	
	private Node getEqualNode(Node x) {
		for (Node n : ship.getNodes()) {
			if (x.equals(n)) {
				if (x != n) System.out.println("Warning: start node should come from ship it is called with");
				return x;
			}
		}
		throw new IllegalArgumentException("Supplied start node doesn't exist in graph");
	}
	
	private Set<Node> getAdjacentNodes(Node n) {
		Set<Node> adjacentNodes = new HashSet<Node>();
		for (Edge e : n.getAdjacent()) {
			adjacentNodes.add(e.other(n));
		}
		return adjacentNodes;
	}
	
	private Edge edgeTo(Node a, Node b) {
		for (Edge e1 : a.getAdjacent()) {
			for (Edge e2 : b.getAdjacent()) {
				if (e1 == e2) return e1;
			}
		}
		throw new IllegalArgumentException(a + " not adjacent to " + b);
	}
}